package com.kaihong.template.ble.operation.impl;

import ohos.bluetooth.ProfileBase;
import ohos.bluetooth.ble.BlePeripheralCallback;
import ohos.bluetooth.ble.BlePeripheralDevice;
import ohos.bluetooth.ble.GattCharacteristic;
import ohos.bluetooth.ble.GattDescriptor;
import ohos.bluetooth.ble.GattService;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import com.kaihong.template.ble.BleProfileUuid;
import com.kaihong.template.ble.operation.BleGattProcessor;
import com.kaihong.template.ble.operation.OptionalConsumer;
import com.kaihong.template.ble.operation.callback.CallbackProvider;
import com.kaihong.template.ble.operation.callback.gatt.BleCharacteristicValueChangeCallback;
import com.kaihong.template.ble.operation.callback.gatt.BleConnectionStateChangeCallback;
import com.kaihong.template.ble.operation.callback.gatt.BleServicesDiscoveredCallback;
import com.kaihong.template.ble.operation.callback.gatt.EnableNotifyIndicateCallback;
import com.kaihong.template.ble.operation.callback.gatt.ReadBleCharacteristicValueCallback;
import com.kaihong.template.ble.operation.callback.gatt.ReadBleDescriptorValueCallback;
import com.kaihong.template.ble.operation.callback.gatt.WriteBleCharacteristicValueCallback;
import com.kaihong.template.ble.operation.callback.gatt.WriteBleDescriptorValueCallback;
import com.kaihong.template.ble.operation.constants.GattCharacteristicConstants;
import com.kaihong.template.ble.operation.constants.GattDescriptorConstants;
import com.kaihong.template.ble.operation.model.DescriptorData;
import com.kaihong.template.ble.utils.BluetoothProfileByteUtil;
import com.kaihong.template.opinterface.OperatorCode;
import com.kaihong.template.utils.Logger;
import com.kaihong.template.utils.UiManager;

/**
 * Bluetooth Low Energy Gatt Operator
 */
public class BleGattOperator implements BleGattProcessor {
    private static final String TAG = "BleGattOperator";

    private static volatile BleGattOperator sInstance;

    private Optional<BlePeripheralDevice> peripheralDevice;

    private CallbackProvider<BleCharacteristicValueChangeCallback> characteristicValueChangeCallback;

    private CallbackProvider<BleConnectionStateChangeCallback> connectionStateChangeCallback;

    private CallbackProvider<BleServicesDiscoveredCallback> discoveredCallback;

    private CallbackProvider<ReadBleCharacteristicValueCallback> readCallback;

    private CallbackProvider<WriteBleCharacteristicValueCallback> writeCallback;

    private CallbackProvider<ReadBleDescriptorValueCallback> readDescriptorCallback;

    private CallbackProvider<WriteBleDescriptorValueCallback> writeDescriptorCallback;

    private CallbackProvider<EnableNotifyIndicateCallback> enableCallback;

    private boolean isEnableIndication = false;
    private boolean isConnected = false;

    private BleGattOperator(BlePeripheralDevice device, BleCharacteristicValueChangeCallback valueChangeCallback,
                            BleConnectionStateChangeCallback stateChangeCallback, BleServicesDiscoveredCallback discoveredCallback,
                            EnableNotifyIndicateCallback enableCallback) {
        peripheralDevice = Optional.ofNullable(device);
        characteristicValueChangeCallback = CallbackProvider.of(valueChangeCallback);
        connectionStateChangeCallback = CallbackProvider.of(stateChangeCallback);
        this.discoveredCallback = CallbackProvider.of(discoveredCallback);
        this.enableCallback = CallbackProvider.of(enableCallback);
    }

    /**
     * use single instance
     *
     * @param device              device
     * @param valueChangeCallback valueChangeCallback
     * @param stateChangeCallback stateChangeCallback
     * @param discoveredCallback  discoveredCallback
     * @param enableCallback      enableCallback
     * @return return single instance
     */
    public static BleGattOperator getInstance(BlePeripheralDevice device,
                                              BleCharacteristicValueChangeCallback valueChangeCallback, BleConnectionStateChangeCallback stateChangeCallback,
                                              BleServicesDiscoveredCallback discoveredCallback, EnableNotifyIndicateCallback enableCallback) {
        if (stateChangeCallback == null) {
            Logger.e(TAG, "BleGattOperator getInstance BleConnectionStateChangeCallback is null.");
        }

        if (sInstance == null) {
            synchronized (BleCentralOperator.class) {
                if (sInstance == null) {
                    sInstance = new BleGattOperator(device, valueChangeCallback, stateChangeCallback,
                            discoveredCallback, enableCallback);
                }
            }
        } else {
            sInstance.peripheralDevice = Optional.ofNullable(device);
        }
        return sInstance;
    }

    @Override
    public int createBleConnection(String deviceId) {
        Logger.i(TAG, "创建连接：createBleConnection()");
        return peripheralDevice.map(device -> device.connect(false, getPeripheralCallback(device))
                ? OperatorCode.ERROR_CODE_OK : OperatorCode.ERROR_CODE_CONNECT_ERR)
                .orElse(OperatorCode.ERROR_CODE_COMMON_ERR);
    }

    @Override
    public int closeBleConnection(String deviceId) {
        int code = peripheralDevice.map(device -> device.disconnect() && device.close() ? OperatorCode.ERROR_CODE_OK
                : OperatorCode.ERROR_CODE_DISCONNECT_ERR).orElse(OperatorCode.ERROR_CODE_COMMON_ERR);
        if (code == 0) {
            this.isConnected = false;
        }
        return code;
    }

    @Override
    public void onBleConnectionStateChange(BleConnectionStateChangeCallback bleConnectionStateChangeCallback) {
        Logger.i(TAG, "onBleConnectionStateChange register callback.");
        this.connectionStateChangeCallback = CallbackProvider.of(bleConnectionStateChangeCallback);
    }

    @Override
    public void onBleServicesDiscovered(BleServicesDiscoveredCallback servicesDiscoveredCallback) {
        Logger.i(TAG, "onBleServicesDiscovered register callback.");
        this.discoveredCallback = CallbackProvider.of(servicesDiscoveredCallback);
        peripheralDevice.ifPresent(BlePeripheralDevice::discoverServices);
    }

    @Override
    public void readBleCharacteristicValue(String deviceId, String serviceId, String characteristicId,
                                           ReadBleCharacteristicValueCallback callback) {
        Logger.i(TAG, "readBleCharacteristicValue register callback.");
        readCallback = CallbackProvider.of(callback);
        peripheralDevice.ifPresent(
                device -> getCharacteristicById(device, serviceId, characteristicId).ifPresent(device::readCharacteristic));
    }

    @Override
    public void writeBleCharacteristicValue(DescriptorData descriptorData,
                                            WriteBleCharacteristicValueCallback callback) {
        Logger.i(TAG, "writeBleCharacteristicValue register callback.");
        writeCallback = CallbackProvider.of(callback);
        peripheralDevice.ifPresent(device -> getWriteCharacteristic(device, descriptorData.getServiceId(),
                descriptorData.getCharacteristicId(), descriptorData.getData()).ifPresent(device::writeCharacteristic));
    }

    @Override
    public int notifyBleCharacteristicValueChange(String deviceId, String serviceId, String characteristicId,
                                                  boolean isEnable) {
        Logger.i(TAG, "notifyBleCharacteristicValueChange.");
        final int[] isStartWrite = {OperatorCode.ERROR_CODE_OK};
        peripheralDevice.ifPresent(device -> device.getService(UUID.fromString(serviceId))
                .flatMap(service -> service.getCharacteristic(UUID.fromString(characteristicId)))
                .ifPresent(characteristic -> isStartWrite[0] =
                        this.enableCharacteristicNotifyOrIndicate(device, characteristic, isEnable)));
        return isStartWrite[0];
    }

    @Override
    public int readBleDescriptorValue(String deviceId, String serviceId, String characteristicId, String descriptorId,
                                      ReadBleDescriptorValueCallback callback) {
        readDescriptorCallback = CallbackProvider.of(callback);
        final boolean[] isStart = {true};
        peripheralDevice.ifPresent(
                device -> getDescriptorById(device, serviceId, characteristicId, descriptorId).ifPresent(descriptor -> {
                    isStart[0] = device.readDescriptor(descriptor);
                }));
        return isStart[0] ? OperatorCode.ERROR_CODE_OK : OperatorCode.ERROR_CODE_COMMON_ERR;
    }

    @Override
    public int writeBleDescriptorValue(DescriptorData descriptorData, WriteBleDescriptorValueCallback callback) {
        writeDescriptorCallback = CallbackProvider.of(callback);
        final boolean[] isStart = {true};
        peripheralDevice.ifPresent(
                device -> getWriteDescriptor(device, descriptorData.getServiceId(), descriptorData.getCharacteristicId(),
                        descriptorData.getDescriptorId(), descriptorData.getData()).ifPresent(descriptor -> {
                    isStart[0] = device.writeDescriptor(descriptor);
                }));
        return isStart[0] ? OperatorCode.ERROR_CODE_OK : OperatorCode.ERROR_CODE_COMMON_ERR;
    }

    @Override
    public void onEnableNotifyIndicate(EnableNotifyIndicateCallback enableNotifyIndicateCallback) {
        Logger.i(TAG, "onEnableNotifyIndicate register callback.");
        this.enableCallback = CallbackProvider.of(enableNotifyIndicateCallback);
    }

    @Override
    public void
    onBleCharacteristicValueChange(BleCharacteristicValueChangeCallback bleCharacteristicValueChangeCallback) {
        Logger.i(TAG, "onBleCharacteristicValueChange register callback.");
        this.characteristicValueChangeCallback = CallbackProvider.of(bleCharacteristicValueChangeCallback);
    }

    @Override
    public void setEnableIndication(boolean enableIndication) {
        Logger.i(TAG, "setEnableIndication.");
        this.isEnableIndication = enableIndication;
    }

    private Optional<GattDescriptor> getWriteDescriptor(BlePeripheralDevice device, String serviceId,
                                                        String characteristicId, String descriptorId, String data) {
        Logger.i(TAG, "getWriteDescriptor enter " + serviceId + " " + characteristicId);
        if (device == null) {
            return Optional.empty();
        }
        List<GattService> services = device.getServices();
        if (services == null) {
            return Optional.empty();
        }
        for (GattService service : services) {
            if (!service.getUuid().toString().equalsIgnoreCase(serviceId)) {
                continue;
            }
            Optional<GattCharacteristic> gattCharacteristic =
                    service.getCharacteristic(UUID.fromString(characteristicId));
            byte[] dataBytes = BluetoothProfileByteUtil.hexToBytes(data);
            Optional<GattDescriptor> gattDescriptor = gattCharacteristic
                    .flatMap(characteristic -> characteristic.getDescriptor(UUID.fromString(descriptorId)));
            gattDescriptor.ifPresent(descriptor -> descriptor.setValue(dataBytes));
            return gattDescriptor;
        }
        return Optional.empty();
    }

    private Optional<GattDescriptor> getDescriptorById(BlePeripheralDevice device, String serviceId,
                                                       String characteristicId, String descriptorId) {
        return getCharacteristicById(device, serviceId, characteristicId)
                .map(characteristic -> characteristic.getDescriptor(UUID.fromString(descriptorId)))
                .orElse(Optional.empty());
    }

    private int enableCharacteristicNotifyOrIndicate(BlePeripheralDevice blePeripheralDevice,
                                                     GattCharacteristic gattCharacteristic, boolean isEnable) {
        byte[] descriptorValue = GattDescriptorConstants.getDisableNotificationValue();
        Logger.i(TAG, "enableCharacteristicNotifyOrIndicate: isEnable = " + isEnable + ", isEnableIndication = "
                + isEnableIndication);
        if (isEnable) {
            descriptorValue = isEnableIndication ? GattDescriptorConstants.getEnableIndicationValue()
                    : GattDescriptorConstants.getEnableNotificationValue();
        }
        final boolean[] isStartWrite = {false};

        // Determine whether the feature supports the corresponding notification or indication type
        int propertyFlag = isEnableIndication ? GattCharacteristicConstants.PROPERTY_INDICATE
                : GattCharacteristicConstants.PROPERTY_NOTIFY;
        Logger.i(TAG, "gattCharacteristic getProperties = " + gattCharacteristic.getProperties());
        if ((gattCharacteristic.getProperties() & propertyFlag) != 0 && blePeripheralDevice != null) {
            blePeripheralDevice.setNotifyCharacteristic(gattCharacteristic, isEnable);
            byte[] finalDescriptorValue = descriptorValue;
            gattCharacteristic.getDescriptor(BleProfileUuid.CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR.getUuid())
                    .ifPresent(descriptor -> {
                        descriptor.setValue(finalDescriptorValue);
                        isStartWrite[0] = blePeripheralDevice.writeDescriptor(descriptor);
                    });
        } else {
            Logger.i(TAG, "gattCharacteristic not support property");
        }
        return isStartWrite[0] ? OperatorCode.ERROR_CODE_OK : OperatorCode.ERROR_CODE_COMMON_ERR;
    }

    private Optional<GattCharacteristic> getWriteCharacteristic(BlePeripheralDevice device, String serviceId,
                                                                String characteristicId, String data) {
        Logger.i(TAG, "getWriteCharacteristic enter " + serviceId + " " + characteristicId);
        if (device == null) {
            return Optional.empty();
        }
        List<GattService> services = device.getServices();
        if (services == null) {
            return Optional.empty();
        }
        for (GattService service : services) {
            if (!service.getUuid().toString().equalsIgnoreCase(serviceId)) {
                continue;
            }
            Optional<GattCharacteristic> gattCharacteristic =
                    service.getCharacteristic(UUID.fromString(characteristicId));
            byte[] dataBytes = BluetoothProfileByteUtil.hexToBytes(data);
            gattCharacteristic.ifPresent(characteristic -> characteristic.setValue(dataBytes));
            Logger.i(TAG, "getWriteCharacteristic: data = " + data + ", dataBytes = " + Arrays.toString(dataBytes));
            return gattCharacteristic;
        }
        return Optional.empty();
    }

    private Optional<GattCharacteristic> getCharacteristicById(
            BlePeripheralDevice device, String serviceId, String characteristicId) {
        if (device == null) {
            return Optional.empty();
        }
        List<GattService> services = device.getServices();
        if (services == null) {
            return Optional.empty();
        }
        for (GattService service : services) {
            if (!service.getUuid().toString().equalsIgnoreCase(serviceId)) {
                continue;
            }
            for (GattCharacteristic characteristic : service.getCharacteristics()) {
                if (characteristic.getUuid().toString().equalsIgnoreCase(characteristicId)) {
                    return Optional.of(characteristic);
                }
            }
        }
        return Optional.empty();
    }

    private BlePeripheralCallback getPeripheralCallback(BlePeripheralDevice device) {
        String deviceId = device.getDeviceAddr();
        return new BlePeripheralCallback() {
            @Override
            public void servicesDiscoveredEvent(int status) {
                super.servicesDiscoveredEvent(status);
                onServicesDiscoveredEvent(status, device);
                if (status == BlePeripheralDevice.OPERATION_SUCC) {
                    Logger.i(TAG, "发现服务成功 servicesDiscoveredEvent()");
                    onConnectionStateChangeEvent(BluetoothProfileByteUtil.STATE_CONNECTED, deviceId);
                }
            }

            @Override
            public void connectionStateChangeEvent(int connectionState) {
                super.connectionStateChangeEvent(connectionState);
                if (connectionState == ProfileBase.STATE_CONNECTED && !isConnected) {
                    Logger.i(TAG, "连接成功 connectionStateChangeEvent() connectionState：" + connectionState);
                    isConnected = true;
                    // 连接成功在外围设备上发现GATT服务，部分手机发现服务失败，需要延迟调用discoverServices()
                    UiManager.getInstance().postEventDelay(() -> {
                        peripheralDevice.ifPresent(BlePeripheralDevice::discoverServices);
                    }, 1000);
                } else if (connectionState == ProfileBase.STATE_DISCONNECTED) {
                    Logger.i(TAG, "断开连接 connectionStateChangeEvent() connectionState：" + connectionState);
                    isConnected = false;
                    onConnectionStateChangeEvent(connectionState, deviceId);
                    closeBleConnection(deviceId);//此处需要调用断开连接，不然再次连接回调会有多次

                }
            }

            @Override
            public void characteristicReadEvent(GattCharacteristic characteristic, int ret) {
                super.characteristicReadEvent(characteristic, ret);
                onCharacteristicReadEvent(characteristic, ret);
            }

            @Override
            public void characteristicWriteEvent(GattCharacteristic characteristic, int ret) {
                super.characteristicWriteEvent(characteristic, ret);
                Logger.i(TAG, "characteristicWriteEvent ret = " + ret);
                writeCallback.get().ifPresent(callback -> callback.onCharacteristicWrite(BluetoothProfileByteUtil.bytesToHexString(characteristic.getValue()), ret));
            }

            @Override
            public void characteristicChangedEvent(GattCharacteristic characteristic) {
                super.characteristicChangedEvent(characteristic);
                Logger.i(TAG, "数据上报 characteristicChangedEvent()：" + BluetoothProfileByteUtil.bytesToHexString(characteristic.getValue()));
                onCharacteristicChangedEvent(characteristic, deviceId);
            }

            @Override
            public void descriptorReadEvent(GattDescriptor descriptor, int ret) {
                super.descriptorReadEvent(descriptor, ret);
                onDescriptorReadEvent(descriptor, ret);
            }

            @Override
            public void descriptorWriteEvent(GattDescriptor descriptor, int ret) {
                super.descriptorWriteEvent(descriptor, ret);
                Logger.i(TAG, "descriptorWriteEvent ret = " + ret);
                enableCallback.get().ifPresent(callback -> callback.onEnableNotifyIndicate(ret));
                writeDescriptorCallback.get().ifPresent(callback -> callback.onDescriptorWrite(ret));
            }
        };
    }

    private void onServicesDiscoveredEvent(int status, BlePeripheralDevice device) {
        Logger.i(TAG, "servicesDiscoveredEvent status = " + status);
        if (status == BlePeripheralDevice.OPERATION_SUCC) {
            List<GattService> services;
            services = device.getServices(); // Obtain the service list after the service is successfully obtained
            if (services == null) {
                Logger.e(TAG, "servicesDiscoveredEvent services is null");
                return;
            }
            for (GattService service : services) {
                if (service == null) {
                    Logger.i(TAG, "servicesDiscoveredEvent service is null");
                } else {
                    Logger.i(TAG, "servicesDiscoveredEvent service = " + service.getUuid());
                }
            }
        }
        OptionalConsumer.of(discoveredCallback.get())
                .ifPresent(callback -> callback.onBleServicesDiscovered(status))
                .ifNotPresent(() -> Logger.e(TAG, "servicesDiscoveredEvent " + "DiscoveredCallback is null"));
    }

    private void onConnectionStateChangeEvent(int connectionState, String deviceId) {
        boolean isConnected = connectionState == BluetoothProfileByteUtil.STATE_CONNECTED;
        Logger.i(TAG, "connectionStateChangeEvent isConnected = " + isConnected);
        OptionalConsumer.of(connectionStateChangeCallback.get())
                .ifPresent(callback -> callback.onBleConnectionStateChange(deviceId, isConnected))
                .ifNotPresent(() -> {
                    Logger.e(TAG, "connectionStateChangeEvent " + "ConnectionStateChangeCallback is null");
                });
    }

    private void onCharacteristicReadEvent(GattCharacteristic characteristic, int ret) {
        Logger.i(TAG, "characteristicReadEvent ret = " + ret);
        readCallback.get()
                .ifPresent(callback -> callback
                        .onCharacteristicRead(BluetoothProfileByteUtil.bytesToHexString(characteristic.getValue()), ret));
    }

    private void onCharacteristicChangedEvent(GattCharacteristic characteristic, String deviceId) {
        Logger.i(TAG, "characteristicChangedEvent" + Arrays.toString(characteristic.getValue()));
        UUID uuid = characteristic.getUuid();
        if (uuid != null) {
            String characteristicId = uuid.toString();
            byte[] data = characteristic.getValue();
            OptionalConsumer.of(characteristicValueChangeCallback.get())
                    .ifPresent(callback -> callback.onBleCharacteristicValueChange(deviceId, characteristicId,
                            BluetoothProfileByteUtil.bytesToHexString(data)))
                    .ifNotPresent(
                            () -> Logger.e(
                                    TAG, "characteristicChangedEvent " + "CharacteristicValueChangeCallback is null"));
        }
    }

    private void onDescriptorReadEvent(GattDescriptor descriptor, int ret) {
        Logger.i(TAG, "descriptorReadEvent ret = " + ret);
        readDescriptorCallback.get()
                .ifPresent(callback -> callback
                        .onDescriptorRead(BluetoothProfileByteUtil.bytesToHexString(descriptor.getValue()), ret));
    }
}
